Initial Commit
This commit is contained in:
221
openspec/archive/2026-05-10-enterprise-cbor/proposal.md
Normal file
221
openspec/archive/2026-05-10-enterprise-cbor/proposal.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# `@suaveplan/cbor` — RFC 8949 CBOR encoder/decoder + RFC 9052 COSE bridge
|
||||
|
||||
**Package**: `@suaveplan/cbor` (NEW)
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (genesis-original; greenfield — no upstream port)
|
||||
**Date**: 2026-05-10
|
||||
**Path**: `packages/vault/cbor/`
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/webauthn` parses CTAP2 authenticator data, attestation statements, and COSE_Key public keys (RFC 9052), every one of which is a CBOR-encoded payload. The package gates on a universal-tier CBOR codec. External alternatives (`cbor-x`, `cbor`) drag in CommonJS interop hazards, oversize transitive deps, and decode behaviors that diverge from RFC 8949 §5.1 strict well-formedness. An in-house, RFC-cited, zero-dependency codec keeps `webauthn` universal-tier and lets every future consumer (CWT per RFC 8392, dCBOR, COSE-keyed vault attachments) share one canonical implementation with one error registry, one OTel surface, and one deterministic-encoding profile.
|
||||
|
||||
## What changes
|
||||
|
||||
### New package: `@suaveplan/cbor`
|
||||
|
||||
- ADDED: `packages/vault/cbor/` package scaffold (`package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`).
|
||||
- ADDED: `src/encoder/encoder.ts` — `Encoder` class and `encode(value, options?): Uint8Array` for RFC 8949 major types 0–7 with preferred-serialization shortest-form integers (§4.2.1).
|
||||
- ADDED: `src/encoder/encoder.types.ts` — `EncoderOptions`, `IndefiniteLengthMarker`, `PreferFloat16` flag, `MaxItems`, `MaxDepth`.
|
||||
- ADDED: `src/encoder/indefinite.ts` — opt-in indefinite-length encoder for arrays / maps / byte strings / text strings (§3.2). Disabled when `deterministic: true`.
|
||||
- ADDED: `src/encoder/half-float.ts` — IEEE-754 binary16 encode helper (major type 7 / value 25). Opt-in via `EncoderOptions.preferFloat16`.
|
||||
- ADDED: `src/encoder/deterministic.ts` — RFC 8949 §4.2.1 + §4.2.2 deterministic profile: shortest-form integers, sorted map keys (lexicographic over canonical CBOR-encoded key bytes), no indefinite-length encodings, half-float disabled.
|
||||
- ADDED: `src/encoder/dcbor.ts` — dCBOR profile (draft-mcnally-deterministic-cbor) exported under the `./deterministic` subpath, layering reduction rules for numeric values and prohibition of `undefined` / `simple-value` over the deterministic baseline.
|
||||
- ADDED: `src/decoder/decoder.ts` — `Decoder` class and `decode(bytes, options?): unknown` for RFC 8949 major types 0–7. Strict mode (default) enforces every §5.1 well-formedness rule.
|
||||
- ADDED: `src/decoder/decoder.types.ts` — `DecoderOptions`, `Tagged<T>`, `DecoderMode = "strict" | "lax"`.
|
||||
- ADDED: `src/decoder/well-formed.ts` — well-formedness checker per §5.1 (`isWellFormed(bytes): boolean`, plus internal helpers used during decode).
|
||||
- ADDED: `src/decoder/half-float.ts` — IEEE-754 binary16 decode (parity with reference implementation; never gated, always available).
|
||||
- ADDED: `src/streaming/pull-decoder.ts` — `PullDecoder` over `Uint8Array` and `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>` exposing `next(): Promise<IteratorResult<unknown>>` so callers materialize one item at a time. Bounded-memory; never buffers the whole stream.
|
||||
- ADDED: `src/sequences/sequences.ts` — RFC 8742 CBOR Sequences encode (`encodeSequence(items, options?): Uint8Array`) and decode (`decodeSequence(bytes, options?): unknown[]`, plus `decodeSequenceStream` async iterator).
|
||||
- ADDED: `src/tags/registry.ts` — `TagRegistry` with `register(tag, handler)` / `unregister(tag)` / `lookup(tag)`. Built-in tags: 0 (RFC 3339 date/time), 1 (epoch), 2 (positive bignum), 3 (negative bignum), 21 (base64url), 22 (base64), 23 (base16), 24 (encoded CBOR), 32 (URI), 35 (regex), 55799 (self-describe).
|
||||
- ADDED: `src/tags/tagged.ts` — `Tagged<T>` value object (`{ tag: number, value: T }`) returned by the decoder for unknown tags in strict mode-with-fallthrough and in lax mode.
|
||||
- ADDED: `src/cose/cose-key.ts` — `decodeCoseKey(bytes): CoseKey` returning the discriminated union `OkpKey | Ec2Key | RsaKey | SymmetricKey` per RFC 9052 §7. Exported under the `./cose` subpath.
|
||||
- ADDED: `src/cose/jwk.ts` — `coseKeyToJwk(key): JsonWebKey` and `jwkToCoseKey(jwk): CoseKey` round-trip helpers.
|
||||
- ADDED: `src/cose/alg-map.ts` — `coseAlgToCryptoAlg(alg)` mapping COSE algorithm labels (-7 ES256, -8 EdDSA, -35 ES384, -36 ES512, -257 RS256, etc.) to genesis-internal `@suaveplan/crypto-curve` algorithm IDs.
|
||||
- ADDED: `src/cose/cose.types.ts` — `CoseKey`, `OkpKey`, `Ec2Key`, `RsaKey`, `SymmetricKey`, `CoseAlg`, `CoseKty`, `CoseCurve`, `CoseKeyOp`. Discriminated by `kty`.
|
||||
- ADDED: `src/diagnostic/diagnostic.ts` — `toDiagnostic(value): string` emitting RFC 8949 §8 diagnostic notation. Marked debug-only in docs; never invoked by encoder / decoder hot paths.
|
||||
- ADDED: `src/errors/error-codes.ts` — `defineCodes("CBOR", [...])` registry with `MALFORMED`, `UNEXPECTED_TYPE`, `UNKNOWN_TAG_STRICT`, `NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`, `NON_DETERMINISTIC` plus `CborError extends SuaveplanError<CborErrorCode>`.
|
||||
- ADDED: `src/observability/metrics.ts` — `cbor.encode_duration_ms` + `cbor.decode_duration_ms` histograms; `cbor.tag_total{tag}` + `cbor.error_total{code}` counters. Wired into encode / decode hot paths.
|
||||
- ADDED: `src/schemas/schemas.ts` — Zod boundary schemas for `EncoderOptions`, `DecoderOptions`, `TagHandlerSchema`, `CoseKeySchema` discriminated union, `JwkSchema`. Validated at every public entry point; failures surface as `CBOR.MALFORMED` (decoder input) or as a Zod-wrapped boundary error (option misuse).
|
||||
- ADDED: `src/__benchmarks__/encode.bench.ts`, `decode.bench.ts`, `cose-key.bench.ts` — mitata benchmarks for 1 KiB COSE_Key, 1 MiB nested map, 100 KiB array of small items.
|
||||
- ADDED: `src/index.ts` — explicit named re-exports of the public surface (no `export *`).
|
||||
|
||||
### Subpath exports
|
||||
|
||||
- `.` — root barrel: encode, decode, `Tagged`, `TagRegistry`, error codes, OTel.
|
||||
- `./encoder` — `Encoder`, `encode`, `EncoderOptions`, half-float helper.
|
||||
- `./decoder` — `Decoder`, `decode`, `DecoderOptions`, well-formedness checker.
|
||||
- `./streaming` — `PullDecoder` (whole-item) + `ChunkDecoder` (partial-item) over `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`.
|
||||
- `./sequences` — RFC 8742 `encodeSequence` / `decodeSequence` / `decodeSequenceStream`.
|
||||
- `./tags` — `TagRegistry`, `Tagged`, built-in tag handlers.
|
||||
- `./deterministic` — RFC 8949 §4.2 deterministic profile + dCBOR profile.
|
||||
- `./cose` — `COSE_Key` parsing, `JWK` round-trip, alg-map, plus `COSE_Sign` / `COSE_Sign1` / `COSE_Encrypt` / `COSE_Encrypt0` / `COSE_Mac` / `COSE_Mac0` encode / decode / verify / decrypt (RFC 9052 §4–§6).
|
||||
- `./cwt` — RFC 8392 CBOR Web Token claim encode / decode / verify with time-window validation.
|
||||
- `./cddl` — RFC 8610 CDDL lexer / parser / validator.
|
||||
- `./diagnostic` — `toDiagnostic` debug emitter.
|
||||
- `./errors` — `CBOR_CODES`, `CborError`, `CborErrorCode`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `@suaveplan/error workspace:^` — `defineCodes`, `SuaveplanError` base.
|
||||
- `@suaveplan/types workspace:^` — shared interface contracts (`Clock` for CWT time validation, base interfaces).
|
||||
- `@suaveplan/crypto-curve workspace:^` — only consumed by `./cose` (`coseAlgToCryptoAlg`, `verifyCoseSign1`) and `./cwt` (`verifyCwt`). Root + `./encoder` + `./decoder` + `./cddl` + `./streaming` + `./sequences` + `./tags` + `./diagnostic` subpaths do NOT depend on `crypto-curve` at runtime.
|
||||
- `@suaveplan/crypto-cipher workspace:^` — only consumed by `./cose` (`decryptCoseEncrypt0`) for AES-GCM / ChaCha20-Poly1305 AEAD primitives.
|
||||
- `@suaveplan/crypto-hash workspace:^` — only consumed by `./cose` (`verifyCoseMac0`) for HMAC-SHA-{256,384,512}.
|
||||
- `@opentelemetry/api catalog:observability` — encode/decode histograms, tag/error counters, COSE / CWT / CDDL operation spans.
|
||||
- `tslib catalog:build`.
|
||||
- `zod catalog:forms` — option boundary validation.
|
||||
- **No** `@noble/*`, `cbor-x`, `cbor`, `@stablelib/*`, browser globals, or Node built-ins. Pure TypeScript over `Uint8Array` + `DataView`.
|
||||
|
||||
## Out of scope (residual)
|
||||
|
||||
Per maintainer instruction, every previously-deferred capability has been folded back IN-scope (CWT, full COSE Sign/Sign1/Encrypt/Encrypt0/Mac/Mac0, CDDL parser+validator, partial-item streaming). The only remaining residual exclusion:
|
||||
|
||||
- **CBOR-LD / SD-CBOR-LD** — distinct semantic-web layer (RDF + JSON-LD context compression) above CBOR; addressed in a separate `@suaveplan/cbor-ld` package when a consumer requests it. **Justification**: CBOR-LD requires a JSON-LD context resolver and an RDF graph model neither of which are CBOR-codec concerns; folding it in would couple this package to a semantic-web stack none of the current or pending consumers need. Not a silent gap.
|
||||
|
||||
## In-scope additions (Wave 2 — folded back per maintainer)
|
||||
|
||||
### CWT (RFC 8392) — `./cwt` subpath
|
||||
|
||||
- ADDED: `src/cwt/claims.ts` — `CwtClaims` discriminated record with the seven IANA-registered claim labels (1 `iss`, 2 `sub`, 3 `aud`, 4 `exp`, 5 `nbf`, 6 `iat`, 7 `cti`) plus a `private[label]` slot for unregistered labels; encoder accepts `Date | number` for time-typed claims and round-trips through `Tagged(1, epoch)` per RFC 8392 §3.
|
||||
- ADDED: `src/cwt/encode.ts` — `encodeCwt(claims, options): Uint8Array` producing a deterministic-encoded claim set ready to be wrapped in `COSE_Sign1` or `COSE_Mac0`.
|
||||
- ADDED: `src/cwt/decode.ts` — `decodeCwt(bytes): CwtClaims` returning the typed claim set; rejects malformed labels with `CBOR.UNEXPECTED_TYPE`.
|
||||
- ADDED: `src/cwt/verify.ts` — `verifyCwt(token, key, options): Promise<CwtVerifyResult>` orchestrating `COSE_Sign1` / `COSE_Mac0` verification via `@suaveplan/crypto-curve` (delegated through the COSE algorithm map) and time-window validation against an injectable `Clock`.
|
||||
- ADDED: `src/cwt/cwt.types.ts` — `CwtClaims`, `CwtVerifyOptions`, `CwtVerifyResult`, `CwtPrivateClaim`.
|
||||
- Errors used: `CBOR.UNEXPECTED_TYPE`, new `CBOR.CWT_EXPIRED`, `CBOR.CWT_NOT_YET_VALID`, `CBOR.CWT_AUDIENCE_MISMATCH`, `CBOR.CWT_ISSUER_MISMATCH`, `CBOR.CWT_SIGNATURE_INVALID`.
|
||||
|
||||
### COSE structures (RFC 9052 §4–§6) — extend `./cose` subpath
|
||||
|
||||
- ADDED: `src/cose/sig-structure.ts` — `buildSigStructure(context, protected, externalAad, payload): Uint8Array` per RFC 9052 §4.4. Five contexts: `"Signature"`, `"Signature1"`, `"CounterSignature"`, `"MAC"`, `"MAC0"`.
|
||||
- ADDED: `src/cose/sign.ts` — `encodeCoseSign({ protected, unprotected, payload, signers }): Uint8Array` and `decodeCoseSign(bytes): CoseSign` covering RFC 9052 §4.1 multi-signer messages (CBOR tag 98).
|
||||
- ADDED: `src/cose/sign1.ts` — `encodeCoseSign1({ protected, unprotected, payload, signature }): Uint8Array` / `decodeCoseSign1(bytes): CoseSign1` / `verifyCoseSign1(msg, key, options): Promise<boolean>` covering RFC 9052 §4.2 single-signer messages (CBOR tag 18). Wires the algorithm-map → `@suaveplan/crypto-curve` ECDSA / EdDSA verifiers.
|
||||
- ADDED: `src/cose/encrypt.ts` — `encodeCoseEncrypt({ protected, unprotected, ciphertext, recipients }): Uint8Array` / `decodeCoseEncrypt(bytes): CoseEncrypt` covering RFC 9052 §5.1 multi-recipient encrypted messages (CBOR tag 96).
|
||||
- ADDED: `src/cose/encrypt0.ts` — `encodeCoseEncrypt0({ protected, unprotected, ciphertext }): Uint8Array` / `decodeCoseEncrypt0(bytes): CoseEncrypt0` / `decryptCoseEncrypt0(msg, key, options): Promise<Uint8Array>` covering RFC 9052 §5.2 single-recipient encrypted messages (CBOR tag 16). Wires AES-GCM / ChaCha20-Poly1305 via `@suaveplan/crypto-cipher`.
|
||||
- ADDED: `src/cose/mac.ts` — `encodeCoseMac({ protected, unprotected, payload, tag, recipients }): Uint8Array` / `decodeCoseMac(bytes): CoseMac` covering RFC 9052 §6.1 multi-recipient MAC messages (CBOR tag 97).
|
||||
- ADDED: `src/cose/mac0.ts` — `encodeCoseMac0({ protected, unprotected, payload, tag }): Uint8Array` / `decodeCoseMac0(bytes): CoseMac0` / `verifyCoseMac0(msg, key, options): Promise<boolean>` covering RFC 9052 §6.2 single-recipient MAC messages (CBOR tag 17). Wires HMAC-SHA-{256,384,512} via `@suaveplan/crypto-hash`.
|
||||
- ADDED: `src/cose/headers.ts` — `CoseHeader` map type, header-label registry covering RFC 9052 §3.1 (`alg`, `crit`, `content type`, `kid`, `IV`, `Partial IV`, `counter signature`).
|
||||
- New errors: `CBOR.COSE_SIG_INVALID`, `CBOR.COSE_AEAD_FAILED`, `CBOR.COSE_HEADER_INVALID`, `CBOR.COSE_RECIPIENT_NOT_FOUND`.
|
||||
|
||||
### CDDL (RFC 8610) parser + validator — `./cddl` subpath
|
||||
|
||||
- ADDED: `src/cddl/lexer.ts` — RFC 8610 ABNF token stream (`identifier`, `=`, `/`, `//`, `*`, `+`, `?`, `..`, `&`, `~`, control operators `.size` `.bits` `.regexp` `.cbor` `.within` `.and` `.lt` `.le` `.gt` `.ge` `.eq` `.ne` `.default`, integer / text-string / byte-string / float literals, comments).
|
||||
- ADDED: `src/cddl/parser.ts` — `parseCddl(source): CddlAst` producing the typed AST per RFC 8610 §3 grammar (rules, types, groups, occurrence indicators, ranges, choices, control operators, generic parameters).
|
||||
- ADDED: `src/cddl/ast.ts` — `CddlAst`, `CddlRule`, `CddlType`, `CddlGroup`, `CddlOccurrence`, `CddlChoice`, `CddlControl`, `CddlGenericParam` discriminated unions.
|
||||
- ADDED: `src/cddl/validate.ts` — `validateCbor(value, schema, options): CddlValidationResult` walking a decoded CBOR value against a CDDL rule by name; emits a structured error path on mismatch (`{ path: (string | number)[], expected, got }`).
|
||||
- ADDED: `src/cddl/built-ins.ts` — RFC 8610 §3.8 prelude (`uint`, `nint`, `int`, `bstr`, `tstr`, `bool`, `nil`, `null`, `undefined`, `tdate`, `time`, `number`, `text`, `bytes`, `float`, `float16`, `float32`, `float64`, `any`).
|
||||
- ADDED: `src/cddl/control-ops.ts` — control-operator implementations (`.size` length bound, `.bits` set membership, `.regexp` pattern match, `.cbor` nested-CBOR bytes, `.within`, `.and`, comparison ops, `.default`).
|
||||
- ADDED: `src/cddl/cddl.types.ts` — option / result types, error path discriminator.
|
||||
- New errors: `CBOR.CDDL_PARSE_FAILED`, `CBOR.CDDL_VALIDATION_FAILED`, `CBOR.CDDL_RULE_NOT_FOUND`, `CBOR.CDDL_UNDEFINED_REFERENCE`.
|
||||
|
||||
### Partial-item streaming — extend `./streaming` subpath
|
||||
|
||||
- ADDED: `src/streaming/chunk-decoder.ts` — `ChunkDecoder` for incremental decode of one CBOR item *larger than memory* over `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`. Emits `{ kind: "string-chunk" | "bytes-chunk" | "array-start" | "array-end" | "map-start" | "map-end" | "tag" | "value" | "break" }` events as the underlying bytes flow in. Bounded memory regardless of item size.
|
||||
- ADDED: `src/streaming/chunk-decoder.types.ts` — `ChunkDecoderEvent` discriminated union, `ChunkDecoderOptions`.
|
||||
- New error: `CBOR.STREAM_INTERRUPTED` for mid-item disconnect.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This package is genesis-original; no `../suaveplan/enterprise/cbor.md` line range exists for any feature. Per maintainer instruction the source-of-record is the RFC itself.
|
||||
|
||||
| Requirement | RFC source | Notes |
|
||||
|---|---|---|
|
||||
| Encoder for major types 0–7 with preferred-serialization shortest-form integers | RFC 8949 §3.1 + §4.2.1 | `unsigned`, `negative`, `byte-string`, `text-string`, `array`, `map`, `tag`, `simple/float` |
|
||||
| Indefinite-length arrays / maps / strings (opt-in) | RFC 8949 §3.2 | Disallowed when `deterministic: true`; throws `INDEFINITE_NOT_ALLOWED` |
|
||||
| Tagged items + built-in tag registry (0, 1, 2, 3, 21, 22, 23, 24, 32, 35, 55799) | RFC 8949 §3.4 + IANA CBOR Tags Registry | Each tag's semantics cited in sibling `.md`; tag 24 (encoded CBOR) re-enters the decoder |
|
||||
| Half-precision float encode (opt-in) + decode | RFC 8949 §3.3 (Major 7 / value 25) + §D | Binary16 IEEE-754; reference test vectors in §A |
|
||||
| Preferred serialization (shortest-form ints, no indefinite) | RFC 8949 §4.1 + §4.2.1 | Always-on; deterministic profile is a strict superset |
|
||||
| Deterministic encoding profile | RFC 8949 §4.2.1 + §4.2.2 | Sorted map keys via lexicographic comparison over canonical encoded key bytes |
|
||||
| dCBOR profile | draft-mcnally-deterministic-cbor (latest) §3 | Reduction rules for numeric values; prohibits `undefined` / generic simple values |
|
||||
| RFC 8742 CBOR Sequences encode + decode | RFC 8742 §2 + §3 | Concatenation of zero-or-more well-formed CBOR items; no enclosing array |
|
||||
| Decoder strict-mode well-formedness | RFC 8949 §5.1 | Includes "no extraneous bytes after item" (§5.1.2) and "all break stops have a matching indefinite start" |
|
||||
| Decoder lax mode | RFC 8949 §5.2 (basic validity) | Lax mode applies §5.2 only; strict mode adds §5.1 |
|
||||
| Streaming pull-decoder | RFC 8949 §3.2 + §5.1 | Bounded memory; consumer drives via `next()` |
|
||||
| Bounded recursion + bounded item count | RFC 8949 §1.2 (security considerations) + §10 | `maxDepth: 64` default → `NESTED_TOO_DEEP`; `maxItems: 1_000_000` default → `LENGTH_OVERFLOW` |
|
||||
| `decodeCoseKey` returning typed key union | RFC 9052 §7 + IANA COSE Key Common Parameters | Discriminator on `kty` (label 1) |
|
||||
| `coseKeyToJwk` / `jwkToCoseKey` round-trip | RFC 9052 §7.1 + RFC 8037 §2 + RFC 7518 §6 | EC2/OKP/RSA/symmetric round-trip |
|
||||
| COSE alg label → genesis algorithm ID | RFC 9053 §2 + IANA COSE Algorithms Registry | -7 ES256, -8 EdDSA, -35 ES384, -36 ES512, -257 RS256, …; rejects unknown labels |
|
||||
| Sig_structure / Enc_structure / MAC_structure builders | RFC 9052 §4.4 + §5.3 + §6.3 | Five contexts: `Signature`, `Signature1`, `CounterSignature`, `MAC`, `MAC0`; deterministic-encoded |
|
||||
| `COSE_Sign` / `COSE_Sign1` encode + decode + verify | RFC 9052 §4.1 + §4.2 + IANA CBOR Tags 98 / 18 | Wires ECDSA / EdDSA via `@suaveplan/crypto-curve` |
|
||||
| `COSE_Encrypt` / `COSE_Encrypt0` encode + decode + decrypt | RFC 9052 §5.1 + §5.2 + IANA CBOR Tags 96 / 16 | Wires AES-GCM / ChaCha20-Poly1305 via `@suaveplan/crypto-cipher` |
|
||||
| `COSE_Mac` / `COSE_Mac0` encode + decode + verify | RFC 9052 §6.1 + §6.2 + IANA CBOR Tags 97 / 17 | Wires HMAC-SHA-{256,384,512} via `@suaveplan/crypto-hash` |
|
||||
| COSE header registry (`alg`, `crit`, `content type`, `kid`, `IV`, `Partial IV`, `counter signature`) | RFC 9052 §3.1 + IANA COSE Header Parameters | Typed `CoseHeader` map; rejects malformed labels |
|
||||
| CWT claim labels (`iss`/`sub`/`aud`/`exp`/`nbf`/`iat`/`cti`) | RFC 8392 §3 + IANA CWT Claims | Typed `CwtClaims` discriminator; epoch tag round-trip |
|
||||
| CWT verify (signature + time window) | RFC 8392 §7 + RFC 9052 §4.2 / §6.2 | Delegates to `verifyCoseSign1` / `verifyCoseMac0`; `Clock` injected |
|
||||
| CDDL lexer + parser | RFC 8610 §3 ABNF grammar | Full prelude; control operators; generic params; comments |
|
||||
| CDDL validator with structured error path | RFC 8610 §3 + §3.8 prelude | `{ path, expected, got }` discriminator on mismatch |
|
||||
| CDDL control operators | RFC 8610 §3.8.1–3.8.16 | `.size`, `.bits`, `.regexp`, `.cbor`, `.within`, `.and`, comparisons, `.default` |
|
||||
| Partial-item streaming (`ChunkDecoder`) | RFC 8949 §3.2 (indefinite-length forms) | Event stream of `string-chunk` / `bytes-chunk` / container-start/end / break; bounded memory regardless of item size |
|
||||
| Diagnostic notation | RFC 8949 §8 | Debug-only emitter; never on hot path |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `defineCodes("CBOR", [...])` 7-code registry | Genesis convention — every package surfaces typed errors via `@suaveplan/error`. The seven codes (`MALFORMED`, `UNEXPECTED_TYPE`, `UNKNOWN_TAG_STRICT`, `NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`, `NON_DETERMINISTIC`) each map to a distinct §5.1 / §10 failure class so callers can branch on root cause. |
|
||||
| Zod boundary schemas at every public option type | Genesis convention (CLAUDE.md §2 / §11). Catches caller misuse at the boundary; option misuse never silently degrades into malformed bytes. |
|
||||
| OTel surface (`cbor.encode_duration_ms`, `cbor.decode_duration_ms`, `cbor.tag_total`, `cbor.error_total`) | Genesis convention — every hot-path package emits histograms + counters. Tag attribute is bounded by IANA registry size; error code attribute is bounded by the 7-entry code registry. |
|
||||
| 300-line file budget + 100/100/100/100 coverage + sibling `.md` per impl file | CLAUDE.md global rules (Quality + Documentation sections). |
|
||||
| Property-based round-trip tests (fast-check) + deterministic-mode commutativity | CLAUDE.md "no mocks" rule; round-trip is the only test that proves encoder + decoder agree on the wire format under arbitrary structures. |
|
||||
| Mitata benchmarks under `src/__benchmarks__/` | CLAUDE.md global benchmark requirement. Baseline numbers feed the README perf table and a future CI regression gate. |
|
||||
| Tag-handler registry pluggability | Future consumer extensibility — `webauthn` may register WebAuthn-specific tags (e.g. AAGUID handling) without modifying this package. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
Each requirement carries a `**Verified by:**` line in the spec delta naming concrete `it(...)` titles inside the test files below.
|
||||
|
||||
- `packages/vault/cbor/src/encoder/encoder.test.ts` — major types 0–7 encode, preferred-serialization shortest-form integers, simple-value encoding, float32 / float64 encode.
|
||||
- `packages/vault/cbor/src/encoder/encoder.spec-conformance.test.ts` — RFC 8949 Appendix A test vectors (every published vector produces the byte sequence in the appendix).
|
||||
- `packages/vault/cbor/src/encoder/indefinite.test.ts` — opt-in indefinite-length emission for arrays / maps / strings; rejected under `deterministic: true`.
|
||||
- `packages/vault/cbor/src/encoder/half-float.test.ts` — binary16 encode for normal, subnormal, ±0, ±Inf, NaN, smallest-normal, largest-normal.
|
||||
- `packages/vault/cbor/src/encoder/deterministic.test.ts` — sorted map keys; shortest-form ints; no indefinite-length output; throws `NON_DETERMINISTIC` when caller forces an indefinite item.
|
||||
- `packages/vault/cbor/src/encoder/dcbor.test.ts` — dCBOR reduction rules; rejects `undefined`; property-based commutativity.
|
||||
- `packages/vault/cbor/src/decoder/decoder.test.ts` — major types 0–7 decode; tagged-value materialization; `Tagged<T>` for unknown tags; strict-mode rejects extraneous trailing bytes.
|
||||
- `packages/vault/cbor/src/decoder/decoder.spec-conformance.test.ts` — RFC 8949 Appendix A round-trip; §5.1 well-formedness rules; lax vs strict.
|
||||
- `packages/vault/cbor/src/decoder/well-formed.test.ts` — `isWellFormed` returns false for every §5.1 failure class.
|
||||
- `packages/vault/cbor/src/decoder/half-float.test.ts` — binary16 decode parity with reference encoder; subnormals; payload-preserving NaN.
|
||||
- `packages/vault/cbor/src/streaming/pull-decoder.test.ts` — pulls one item per `next()`; bounded memory under a 1 GiB sequence (test asserts heap delta upper bound); cancels on `signal.aborted`.
|
||||
- `packages/vault/cbor/src/sequences/sequences.test.ts` — RFC 8742 encode + decode; empty sequence; mixed-major-type sequence; partial-trailing-byte rejection in strict mode.
|
||||
- `packages/vault/cbor/src/tags/registry.test.ts` — register / lookup / unregister; double-register rejects; unknown tag in strict-fallthrough returns `Tagged`.
|
||||
- `packages/vault/cbor/src/tags/builtin.test.ts` — every built-in tag (0, 1, 2, 3, 21, 22, 23, 24, 32, 35, 55799) round-trips through the registry.
|
||||
- `packages/vault/cbor/src/cose/cose-key.test.ts` — RFC 9052 §7 sample EC2, OKP, RSA, symmetric keys decode to the typed union; `kty` discriminator works.
|
||||
- `packages/vault/cbor/src/cose/jwk.test.ts` — ES256 EC2 ↔ JWK round-trip; Ed25519 OKP ↔ JWK round-trip; RS256 RSA ↔ JWK round-trip (with `n`/`e` base64url encoding); symmetric key round-trip.
|
||||
- `packages/vault/cbor/src/cose/alg-map.test.ts` — every IANA-registered alg label this package supports maps to the right `@suaveplan/crypto-curve` ID; unknown label throws `UNEXPECTED_TYPE`.
|
||||
- `packages/vault/cbor/src/cose/sig-structure.test.ts` — RFC 9052 §4.4 vectors for all five contexts produce byte-identical canonical output.
|
||||
- `packages/vault/cbor/src/cose/sign1.test.ts` — RFC 9052 Appendix C.2 ES256 / EdDSA test vectors round-trip; tampered signature rejected with `COSE_SIG_INVALID`; tag-prefix and tagless inputs both supported.
|
||||
- `packages/vault/cbor/src/cose/sign.test.ts` — multi-signer encode + decode; per-signer header preserved; verify-any returns the matching signer index.
|
||||
- `packages/vault/cbor/src/cose/encrypt0.test.ts` — RFC 9052 Appendix C.3 AES-GCM-256 vector decrypts; tampered ciphertext rejected with `COSE_AEAD_FAILED`; ChaCha20-Poly1305 round-trip.
|
||||
- `packages/vault/cbor/src/cose/encrypt.test.ts` — multi-recipient encode + decode; per-recipient KEK lookup.
|
||||
- `packages/vault/cbor/src/cose/mac0.test.ts` — HMAC-SHA-256 / 384 / 512 round-trip; tampered MAC rejected.
|
||||
- `packages/vault/cbor/src/cose/mac.test.ts` — multi-recipient MAC round-trip.
|
||||
- `packages/vault/cbor/src/cose/headers.test.ts` — header label registry round-trip; malformed label rejected with `COSE_HEADER_INVALID`.
|
||||
- `packages/vault/cbor/src/cwt/claims.test.ts` — every IANA claim label round-trips; `Date` ↔ epoch via tag 1; private claims preserved.
|
||||
- `packages/vault/cbor/src/cwt/encode.test.ts` — deterministic claim-set encoding; canonical bytes stable across two runs.
|
||||
- `packages/vault/cbor/src/cwt/decode.test.ts` — RFC 8392 Appendix A vectors decode to typed claims.
|
||||
- `packages/vault/cbor/src/cwt/verify.test.ts` — `COSE_Sign1`-wrapped CWT verifies; expired token throws `CWT_EXPIRED`; `nbf` future throws `CWT_NOT_YET_VALID`; audience-mismatch throws `CWT_AUDIENCE_MISMATCH`.
|
||||
- `packages/vault/cbor/src/cddl/lexer.test.ts` — RFC 8610 §3 token-stream coverage including comments, control operators, ranges.
|
||||
- `packages/vault/cbor/src/cddl/parser.test.ts` — every grammar production from RFC 8610 §3; rejects malformed source with `CDDL_PARSE_FAILED` carrying line / column.
|
||||
- `packages/vault/cbor/src/cddl/validate.test.ts` — RFC 8610 Appendix examples (CWT schema, COSE_Sign schema) validate against real RFC vectors; mismatch surfaces `{ path, expected, got }`.
|
||||
- `packages/vault/cbor/src/cddl/built-ins.test.ts` — every prelude type accepts its conforming values and rejects non-conforming.
|
||||
- `packages/vault/cbor/src/cddl/control-ops.test.ts` — every control operator (`.size`, `.bits`, `.regexp`, `.cbor`, `.within`, `.and`, comparisons, `.default`) tested with conforming + non-conforming input.
|
||||
- `packages/vault/cbor/src/streaming/chunk-decoder.test.ts` — emits expected event sequence for an indefinite-length text-string in 3 chunks; bounded heap delta over a 10 GiB synthetic indefinite byte-string; aborts mid-item with `STREAM_INTERRUPTED`.
|
||||
- `packages/vault/cbor/src/diagnostic/diagnostic.test.ts` — RFC 8949 §8 sample diagnostic strings emit byte-identical output.
|
||||
- `packages/vault/cbor/src/errors/error-codes.test.ts` — every code in the registry is thrown by ≥1 production code path; `expectTypeOf<CborErrorCode>().toEqualTypeOf<…>()` pins the literal union.
|
||||
- `packages/vault/cbor/src/observability/metrics.test.ts` — encode/decode histograms record under a real in-memory `MeterProvider`; `cbor.tag_total{tag}` increments per decoded tag; `cbor.error_total{code}` increments on every thrown `CborError`.
|
||||
- `packages/vault/cbor/src/schemas/schemas.test.ts` — every Zod schema rejects malformed options with the genesis boundary error.
|
||||
- `packages/vault/cbor/src/encoder/property.test.ts` — fast-check round-trip: arbitrary `JsonValue`-like structures encode → decode → byte-identical.
|
||||
- `packages/vault/cbor/src/encoder/deterministic-property.test.ts` — fast-check: deterministic encoder emits byte-identical output across two runs on shuffled-key maps.
|
||||
- `packages/vault/cbor/src/index.test.ts` — every documented subpath export resolves.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only; this is the first delta of a new package. No existing code in genesis imports from `@suaveplan/cbor`. `@suaveplan/webauthn` is the gating consumer and lands in a follow-up change once this package archives; that change will add the dep edge and migrate `webauthn`'s CTAP2 / attestation parsers off any external CBOR placeholder.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`bunx turbo ls --filter=...@suaveplan/cbor` returns no current dependents (the package does not yet exist on `genesis`). The pending consumer is:
|
||||
|
||||
- `@suaveplan/webauthn` — [ ] tests run / [ ] docs reviewed / [x] no action needed in THIS change (reason: webauthn is itself pending migration; consumer wiring lands in the webauthn migration change once that proposal is opened, per `STATUS.md`).
|
||||
|
||||
No foundation-package fan-out applies (this is a new universal-tier leaf package; foundation deps `@suaveplan/error` and `@suaveplan/types` are unchanged).
|
||||
520
openspec/archive/2026-05-10-enterprise-cbor/specs/cbor/spec.md
Normal file
520
openspec/archive/2026-05-10-enterprise-cbor/specs/cbor/spec.md
Normal file
@@ -0,0 +1,520 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: cbor (enterprise-cbor)
|
||||
|
||||
**Package**: `@suaveplan/cbor`
|
||||
**Status**: PROPOSED — first delta; becomes canonical spec on archive.
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: RFC 8949 encoder for major types 0–7 with preferred-serialization shortest-form integers
|
||||
|
||||
The package SHALL export an `Encoder` class and an `encode(value, options?): Uint8Array` function that produce RFC 8949 §3.1 byte sequences for every major type: 0 unsigned integer, 1 negative integer, 2 byte string, 3 text string, 4 array, 5 map, 6 tagged item, 7 simple/float. Integers SHALL use the preferred-serialization shortest-form rule (RFC 8949 §4.2.1) — the smallest of 1 / 2 / 3 / 5 / 9 byte representations that fits the value, regardless of the input numeric type. Floats SHALL encode as `float64` by default; `float32` SHALL be emitted only when the value round-trips through `float32` losslessly; `float16` SHALL be emitted only when `EncoderOptions.preferFloat16: true` AND the value round-trips through binary16 losslessly.
|
||||
|
||||
#### Scenario: encodes unsigned integer 0 to single byte 0x00
|
||||
|
||||
- **WHEN** `encode(0)` is called
|
||||
- **THEN** the result equals `Uint8Array([0x00])`
|
||||
|
||||
#### Scenario: encodes unsigned integer 23 to single byte 0x17
|
||||
|
||||
- **WHEN** `encode(23)` is called
|
||||
- **THEN** the result equals `Uint8Array([0x17])`
|
||||
|
||||
#### Scenario: encodes unsigned integer 24 to two bytes 0x18 0x18
|
||||
|
||||
- **WHEN** `encode(24)` is called
|
||||
- **THEN** the result equals `Uint8Array([0x18, 0x18])`
|
||||
|
||||
#### Scenario: encodes negative integer -1 to single byte 0x20
|
||||
|
||||
- **WHEN** `encode(-1)` is called
|
||||
- **THEN** the result equals `Uint8Array([0x20])`
|
||||
|
||||
#### Scenario: encodes empty array to single byte 0x80
|
||||
|
||||
- **WHEN** `encode([])` is called
|
||||
- **THEN** the result equals `Uint8Array([0x80])`
|
||||
|
||||
#### Scenario: encodes empty map to single byte 0xa0
|
||||
|
||||
- **WHEN** `encode(new Map())` is called
|
||||
- **THEN** the result equals `Uint8Array([0xa0])`
|
||||
|
||||
#### Scenario: integer 1000000 encodes to 5-byte form not 9-byte
|
||||
|
||||
- **WHEN** `encode(1_000_000)` is called
|
||||
- **THEN** the result is 5 bytes long with leading byte `0x1a`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 0"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 23 in single byte"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes unsigned integer 24 in two bytes"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes negative integer -1"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes empty array"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes empty map"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"encodes 1000000 in shortest 5-byte form"` AND `packages/vault/cbor/src/encoder/encoder.spec-conformance.test.ts::"RFC 8949 Appendix A vectors round-trip"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Indefinite-length encoding (opt-in)
|
||||
|
||||
The package SHALL emit indefinite-length arrays, maps, byte strings, and text strings (RFC 8949 §3.2) only when the caller passes `EncoderOptions.indefiniteLength: true`. With the option off (default), the encoder SHALL emit definite-length forms for every container. Under the deterministic profile (`deterministic: true`), indefinite-length encoding SHALL be rejected: callers passing both `indefiniteLength: true` and `deterministic: true` SHALL receive a `CborError(CBOR.INDEFINITE_NOT_ALLOWED)` at the boundary.
|
||||
|
||||
#### Scenario: indefinite-length emission opt-in
|
||||
|
||||
- **WHEN** `encode([1, 2, 3], { indefiniteLength: true })` is called
|
||||
- **THEN** the first byte equals `0x9f` (indefinite-length array start) and the last byte equals `0xff` (break)
|
||||
|
||||
#### Scenario: definite-length is the default
|
||||
|
||||
- **WHEN** `encode([1, 2, 3])` is called
|
||||
- **THEN** the first byte equals `0x83` (definite-length array of 3) and there is no break stop
|
||||
|
||||
#### Scenario: indefinite + deterministic is rejected
|
||||
|
||||
- **WHEN** `encode([1, 2, 3], { indefiniteLength: true, deterministic: true })` is called
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.INDEFINITE_NOT_ALLOWED"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/indefinite.test.ts::"emits indefinite-length array when opt-in"` AND `packages/vault/cbor/src/encoder/indefinite.test.ts::"defaults to definite-length"` AND `packages/vault/cbor/src/encoder/indefinite.test.ts::"throws INDEFINITE_NOT_ALLOWED under deterministic"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Tagged items with built-in registry
|
||||
|
||||
The package SHALL encode and decode tagged items (RFC 8949 §3.4 / major type 6) and SHALL ship a `TagRegistry` with built-in handlers for IANA-registered tags 0 (RFC 3339 date/time), 1 (epoch-based date/time), 2 (positive bignum), 3 (negative bignum), 21 (expected base64url), 22 (expected base64), 23 (expected base16), 24 (encoded CBOR — re-enters the decoder), 32 (URI), 35 (regex), and 55799 (self-describe CBOR). Consumers SHALL be able to register custom handlers via `registry.register(tag, handler)`. Per RFC 8949 §3.4, an unknown tag SHALL NOT cause decode failure — the decoder SHALL return a `Tagged<T>` value object so the caller can decide.
|
||||
|
||||
#### Scenario: tag 0 round-trips an RFC 3339 date string
|
||||
|
||||
- **WHEN** `encode(new Date("2026-05-10T00:00:00Z"))` is called and the result is decoded
|
||||
- **THEN** the decoded value is a `Date` whose ISO string equals `"2026-05-10T00:00:00.000Z"`
|
||||
|
||||
#### Scenario: tag 24 (encoded CBOR) re-enters the decoder
|
||||
|
||||
- **GIVEN** a tag 24 wrapping the byte string for `encode(42)`
|
||||
- **WHEN** the wrapper is decoded
|
||||
- **THEN** the result equals `42` (the inner CBOR was decoded transparently)
|
||||
|
||||
#### Scenario: unknown tag in strict mode returns Tagged
|
||||
|
||||
- **GIVEN** a CBOR payload with tag number 9999 wrapping the string `"hi"`
|
||||
- **WHEN** `decode(bytes)` is called with default `mode: "strict"`
|
||||
- **THEN** the result is a `Tagged` with `{ tag: 9999, value: "hi" }`
|
||||
|
||||
#### Scenario: registering a handler intercepts decode
|
||||
|
||||
- **GIVEN** a `TagRegistry` with `register(9999, (v) => ({ wrapped: v }))`
|
||||
- **WHEN** the same payload is decoded through that registry
|
||||
- **THEN** the result equals `{ wrapped: "hi" }`
|
||||
|
||||
#### Scenario: double-register throws
|
||||
|
||||
- **WHEN** `registry.register(0, handler)` is called twice for the same tag
|
||||
- **THEN** the second call throws `CborError` with code `"CBOR.UNEXPECTED_TYPE"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/tags/builtin.test.ts::"tag 0 round-trips RFC 3339 date"` AND `packages/vault/cbor/src/tags/builtin.test.ts::"tag 24 re-enters the decoder"` AND `packages/vault/cbor/src/tags/registry.test.ts::"unknown tag in strict mode returns Tagged"` AND `packages/vault/cbor/src/tags/registry.test.ts::"registered handler intercepts decode"` AND `packages/vault/cbor/src/tags/registry.test.ts::"double-register throws"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Half-precision float encode (opt-in) and decode
|
||||
|
||||
The package SHALL decode IEEE-754 binary16 values (RFC 8949 §3.3, major type 7 / value 25) — always available, never gated. The package SHALL encode binary16 only when `EncoderOptions.preferFloat16: true` AND the value round-trips through binary16 losslessly. Subnormals, ±0, ±Inf, and quiet/signalling NaN payloads SHALL all decode correctly per RFC 8949 §D.
|
||||
|
||||
#### Scenario: decodes 0xf90000 to +0
|
||||
|
||||
- **WHEN** `decode(Uint8Array([0xf9, 0x00, 0x00]))` is called
|
||||
- **THEN** the result equals `0` and `Object.is(result, 0)` is true (positive zero)
|
||||
|
||||
#### Scenario: decodes 0xf98000 to -0
|
||||
|
||||
- **WHEN** `decode(Uint8Array([0xf9, 0x80, 0x00]))` is called
|
||||
- **THEN** the result satisfies `Object.is(result, -0)`
|
||||
|
||||
#### Scenario: decodes 0xf97c00 to +Infinity
|
||||
|
||||
- **WHEN** `decode(Uint8Array([0xf9, 0x7c, 0x00]))` is called
|
||||
- **THEN** the result equals `Number.POSITIVE_INFINITY`
|
||||
|
||||
#### Scenario: encodes 1.0 as binary16 when opt-in
|
||||
|
||||
- **WHEN** `encode(1.0, { preferFloat16: true })` is called
|
||||
- **THEN** the result equals `Uint8Array([0xf9, 0x3c, 0x00])`
|
||||
|
||||
#### Scenario: encoder skips binary16 when value would lose precision
|
||||
|
||||
- **WHEN** `encode(1.1, { preferFloat16: true })` is called
|
||||
- **THEN** the result is NOT a 3-byte half-float — it is the float32 or float64 form that preserves `1.1`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/decoder/half-float.test.ts::"decodes binary16 +0"` AND `packages/vault/cbor/src/decoder/half-float.test.ts::"decodes binary16 -0"` AND `packages/vault/cbor/src/decoder/half-float.test.ts::"decodes binary16 +Infinity"` AND `packages/vault/cbor/src/encoder/half-float.test.ts::"encodes 1.0 as binary16 when opt-in"` AND `packages/vault/cbor/src/encoder/half-float.test.ts::"skips binary16 when value would lose precision"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Deterministic encoding profile (RFC 8949 §4.2)
|
||||
|
||||
The package SHALL provide a deterministic encoding profile gated by `EncoderOptions.deterministic: true` that enforces every rule in RFC 8949 §4.2.1 + §4.2.2: integers use shortest-form encoding, floats use the smallest losslessly-representable form (float64 → float32 → no half-float — half-float disabled in deterministic mode), maps use sorted keys via lexicographic comparison over canonical encoded key bytes, and indefinite-length encodings are forbidden. Two runs of the deterministic encoder over the same logical structure (with map-key ordering shuffled) SHALL produce byte-identical output.
|
||||
|
||||
#### Scenario: map keys are sorted lexicographically over encoded key bytes
|
||||
|
||||
- **WHEN** `encode(new Map([["b", 1], ["a", 2]]), { deterministic: true })` is called
|
||||
- **THEN** the resulting bytes encode `"a"` before `"b"` regardless of insertion order
|
||||
|
||||
#### Scenario: integer key 10 sorts after integer key 2 by encoded-byte length
|
||||
|
||||
- **WHEN** a map containing keys `2` and `10` is encoded under deterministic mode
|
||||
- **THEN** key `2` (single byte 0x02) precedes key `10` (two bytes 0x18 0x0a) per RFC 8949 §4.2.1 length-then-lexicographic ordering
|
||||
|
||||
#### Scenario: half-float disabled under deterministic profile
|
||||
|
||||
- **WHEN** `encode(1.0, { deterministic: true, preferFloat16: true })` is called
|
||||
- **THEN** the result is the float32 form (5 bytes starting `0xfa`), not the binary16 form (3 bytes starting `0xf9`)
|
||||
|
||||
#### Scenario: shuffled-key map produces byte-identical output across runs
|
||||
|
||||
- **GIVEN** a fast-check arbitrary that generates the same logical map with shuffled insertion order
|
||||
- **WHEN** both shuffles are encoded under `deterministic: true`
|
||||
- **THEN** the two `Uint8Array` outputs are byte-identical
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/deterministic.test.ts::"sorts map keys lexicographically by encoded bytes"` AND `packages/vault/cbor/src/encoder/deterministic.test.ts::"sorts integer keys by length then value"` AND `packages/vault/cbor/src/encoder/deterministic.test.ts::"disables half-float under deterministic profile"` AND `packages/vault/cbor/src/encoder/deterministic-property.test.ts::"shuffled-key map produces byte-identical deterministic output"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: dCBOR profile (draft-mcnally-deterministic-cbor)
|
||||
|
||||
The package SHALL provide an opt-in dCBOR profile under the `./deterministic` subpath that layers the dCBOR draft's reduction rules over the RFC 8949 §4.2 deterministic baseline: numeric values SHALL collapse to the smallest type that represents them losslessly (e.g. `1.0` collapses to integer `1`), and `undefined` and unknown simple values SHALL be rejected with `CborError(CBOR.NON_DETERMINISTIC)`. dCBOR output SHALL be a strict subset of RFC 8949 §4.2 deterministic output and SHALL satisfy every scenario of the prior requirement.
|
||||
|
||||
#### Scenario: numeric reduction collapses 1.0 to integer 1
|
||||
|
||||
- **WHEN** `encodeDcbor(1.0)` is called
|
||||
- **THEN** the result equals `Uint8Array([0x01])` (major type 0 unsigned integer 1)
|
||||
|
||||
#### Scenario: rejects undefined value
|
||||
|
||||
- **WHEN** `encodeDcbor(undefined)` is called
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.NON_DETERMINISTIC"`
|
||||
|
||||
#### Scenario: dCBOR output is RFC 8949 deterministic
|
||||
|
||||
- **GIVEN** any non-rejected input
|
||||
- **WHEN** `encodeDcbor(value)` is called and the bytes are also produced via `encode(value, { deterministic: true })`
|
||||
- **THEN** the dCBOR output is byte-identical to the deterministic output OR is a strict reduction of it (numeric collapse rule)
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/dcbor.test.ts::"reduces 1.0 to integer 1"` AND `packages/vault/cbor/src/encoder/dcbor.test.ts::"rejects undefined as NON_DETERMINISTIC"` AND `packages/vault/cbor/src/encoder/dcbor.test.ts::"dCBOR output satisfies RFC 8949 deterministic invariants"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RFC 8742 CBOR Sequences
|
||||
|
||||
The package SHALL encode and decode CBOR Sequences per RFC 8742 §2 and §3 — the concatenation of zero or more well-formed CBOR data items with no enclosing array. The package SHALL export `encodeSequence(items, options?): Uint8Array`, `decodeSequence(bytes, options?): unknown[]`, and `decodeSequenceStream(stream, options?): AsyncIterable<unknown>` for incremental consumption. An empty input SHALL produce an empty `Uint8Array` on encode and an empty array (or empty stream) on decode.
|
||||
|
||||
#### Scenario: encodes mixed-major-type sequence
|
||||
|
||||
- **WHEN** `encodeSequence([1, "two", [3]])` is called
|
||||
- **THEN** the result equals the concatenation of `encode(1)`, `encode("two")`, and `encode([3])`
|
||||
|
||||
#### Scenario: decodes the same bytes back to the same array
|
||||
|
||||
- **GIVEN** the bytes from the prior scenario
|
||||
- **WHEN** `decodeSequence(bytes)` is called
|
||||
- **THEN** the result deeply equals `[1, "two", [3]]`
|
||||
|
||||
#### Scenario: empty sequence round-trips
|
||||
|
||||
- **WHEN** `encodeSequence([])` is called and the result is decoded
|
||||
- **THEN** the round-trip yields `[]` and the encoded byte length equals 0
|
||||
|
||||
#### Scenario: stream form yields one item at a time
|
||||
|
||||
- **GIVEN** an `AsyncIterable<Uint8Array>` chunking the bytes from scenario 1
|
||||
- **WHEN** `decodeSequenceStream(stream)` is iterated
|
||||
- **THEN** three items are yielded in order: `1`, `"two"`, `[3]`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/sequences/sequences.test.ts::"encodes mixed-major-type sequence"` AND `packages/vault/cbor/src/sequences/sequences.test.ts::"decodes sequence back to source array"` AND `packages/vault/cbor/src/sequences/sequences.test.ts::"empty sequence round-trips"` AND `packages/vault/cbor/src/sequences/sequences.test.ts::"stream form yields one item at a time"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Decoder strict mode and well-formedness checks (RFC 8949 §5.1)
|
||||
|
||||
The package SHALL export a `Decoder` class and `decode(bytes, options?): unknown` function. Strict mode (default) SHALL enforce every well-formedness rule from RFC 8949 §5.1: no extraneous trailing bytes after a complete item, every break stop matches an indefinite-length container start, every indefinite-length text/byte string contains only definite-length chunks of the same major type, integer / length encodings use a valid initial-byte form, and reserved simple values are rejected. Lax mode (`options.mode: "lax"`) SHALL apply only RFC 8949 §5.2 basic validity. Strict-mode failures SHALL throw `CborError(CBOR.MALFORMED)`.
|
||||
|
||||
#### Scenario: strict mode rejects extraneous trailing bytes
|
||||
|
||||
- **GIVEN** the bytes `[0x01, 0xff]` (valid integer 1 followed by stray break)
|
||||
- **WHEN** `decode(bytes)` is called with default mode
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.MALFORMED"`
|
||||
|
||||
#### Scenario: lax mode tolerates extraneous trailing bytes
|
||||
|
||||
- **GIVEN** the same bytes
|
||||
- **WHEN** `decode(bytes, { mode: "lax" })` is called
|
||||
- **THEN** the result equals `1` and no error is thrown
|
||||
|
||||
#### Scenario: strict mode rejects indefinite-string with wrong-major-type chunk
|
||||
|
||||
- **GIVEN** an indefinite-length text string (`0x7f`) containing a byte-string chunk
|
||||
- **WHEN** `decode(bytes)` is called
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.MALFORMED"`
|
||||
|
||||
#### Scenario: isWellFormed returns false for every §5.1 failure class
|
||||
|
||||
- **WHEN** `isWellFormed` is invoked over a fixture array of malformed payloads (one per §5.1 rule)
|
||||
- **THEN** every fixture returns `false`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/decoder/decoder.test.ts::"strict mode rejects extraneous trailing bytes"` AND `packages/vault/cbor/src/decoder/decoder.test.ts::"lax mode tolerates extraneous trailing bytes"` AND `packages/vault/cbor/src/decoder/decoder.spec-conformance.test.ts::"strict mode rejects indefinite text with wrong-major chunk"` AND `packages/vault/cbor/src/decoder/well-formed.test.ts::"isWellFormed returns false for every §5.1 failure class"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Streaming pull-decoder over Uint8Array and ReadableStream
|
||||
|
||||
The package SHALL export a `PullDecoder` that consumes either a `Uint8Array` or an `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>` and exposes `next(): Promise<IteratorResult<unknown>>` so callers materialize one CBOR item at a time. Memory usage SHALL be bounded — the decoder SHALL NOT buffer the entire input before yielding. The decoder SHALL honor `AbortSignal`: when the signal aborts mid-item, the next `next()` call SHALL reject with `CborError(CBOR.MALFORMED)` and SHALL release any internal buffers.
|
||||
|
||||
#### Scenario: pulls one item per next() call
|
||||
|
||||
- **GIVEN** an input encoding `[1, 2, 3]` as a CBOR Sequence
|
||||
- **WHEN** `next()` is called three times
|
||||
- **THEN** the calls resolve to `{ value: 1, done: false }`, `{ value: 2, done: false }`, `{ value: 3, done: false }` in order, followed by `{ value: undefined, done: true }`
|
||||
|
||||
#### Scenario: bounded memory under a 1 GiB sequence
|
||||
|
||||
- **GIVEN** a synthetic stream emitting one million 1 KiB CBOR items
|
||||
- **WHEN** the consumer iterates with `next()` and discards every result
|
||||
- **THEN** the peak heap delta during iteration is bounded by a fixed constant (asserted via `process.memoryUsage().heapUsed` snapshot) regardless of total stream length
|
||||
|
||||
#### Scenario: AbortSignal cancellation rejects next()
|
||||
|
||||
- **GIVEN** a `PullDecoder` constructed with an `AbortSignal`
|
||||
- **WHEN** the signal is aborted mid-stream and `next()` is called
|
||||
- **THEN** the returned promise rejects with `CborError` whose code equals `"CBOR.MALFORMED"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/streaming/pull-decoder.test.ts::"pulls one item per next() call"` AND `packages/vault/cbor/src/streaming/pull-decoder.test.ts::"bounded memory under 1 GiB sequence"` AND `packages/vault/cbor/src/streaming/pull-decoder.test.ts::"AbortSignal cancellation rejects next()"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Bounded recursion and bounded item count
|
||||
|
||||
The encoder and decoder SHALL enforce caller-configurable security bounds per RFC 8949 §10: `maxDepth` (default `64`) caps nested container depth and overflows SHALL throw `CborError(CBOR.NESTED_TOO_DEEP)`; `maxItems` (default `1_000_000`) caps the total number of items materialized in a single decode (across nested arrays / maps / sequences) and overflows SHALL throw `CborError(CBOR.LENGTH_OVERFLOW)`. Both caps SHALL be configurable via `EncoderOptions` / `DecoderOptions`. The defaults SHALL apply when the caller passes no override.
|
||||
|
||||
#### Scenario: depth overflow throws NESTED_TOO_DEEP
|
||||
|
||||
- **GIVEN** an array nested 65 levels deep
|
||||
- **WHEN** `encode(value)` is called with default `maxDepth: 64`
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.NESTED_TOO_DEEP"`
|
||||
|
||||
#### Scenario: item-count overflow throws LENGTH_OVERFLOW
|
||||
|
||||
- **GIVEN** a CBOR payload claiming an indefinite array of 1_000_001 small items
|
||||
- **WHEN** `decode(bytes)` is called with default `maxItems: 1_000_000`
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.LENGTH_OVERFLOW"`
|
||||
|
||||
#### Scenario: configurable override increases the cap
|
||||
|
||||
- **GIVEN** a 100-deep nested array
|
||||
- **WHEN** `encode(value, { maxDepth: 128 })` is called
|
||||
- **THEN** the call succeeds with no error
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/encoder.test.ts::"throws NESTED_TOO_DEEP at default depth 64"` AND `packages/vault/cbor/src/decoder/decoder.test.ts::"throws LENGTH_OVERFLOW at default cap"` AND `packages/vault/cbor/src/encoder/encoder.test.ts::"honors caller-supplied maxDepth override"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: COSE_Key decoder (RFC 9052 §7)
|
||||
|
||||
The package SHALL export `decodeCoseKey(bytes): CoseKey` from the `./cose` subpath that decodes a COSE_Key map per RFC 9052 §7 into a discriminated union `OkpKey | Ec2Key | RsaKey | SymmetricKey`. The discriminator SHALL be the `kty` parameter (label 1): `1` → OKP (RFC 8037), `2` → EC2 (RFC 9052 §7.1), `3` → RSA (RFC 8230), `4` → Symmetric. Each variant SHALL include the structurally-typed parameters required by its key type (`crv`, `x`, `y`, `d`, `n`, `e`, `k` as appropriate). Malformed input SHALL throw `CborError(CBOR.MALFORMED)`; unknown `kty` SHALL throw `CborError(CBOR.UNEXPECTED_TYPE)`.
|
||||
|
||||
#### Scenario: decodes EC2 P-256 public key
|
||||
|
||||
- **GIVEN** the canonical RFC 9052 §7.1 sample EC2 P-256 public key bytes
|
||||
- **WHEN** `decodeCoseKey(bytes)` is called
|
||||
- **THEN** the result is `{ kty: "EC2", crv: "P-256", x: <Uint8Array>, y: <Uint8Array> }` and TypeScript narrows the type to `Ec2Key`
|
||||
|
||||
#### Scenario: decodes Ed25519 OKP public key
|
||||
|
||||
- **GIVEN** an OKP public key with `crv: Ed25519`
|
||||
- **WHEN** `decodeCoseKey(bytes)` is called
|
||||
- **THEN** the result is `{ kty: "OKP", crv: "Ed25519", x: <Uint8Array> }`
|
||||
|
||||
#### Scenario: rejects unknown kty
|
||||
|
||||
- **GIVEN** a COSE_Key map with `kty: 99`
|
||||
- **WHEN** `decodeCoseKey(bytes)` is called
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.UNEXPECTED_TYPE"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/cose/cose-key.test.ts::"decodes EC2 P-256 public key"` AND `packages/vault/cbor/src/cose/cose-key.test.ts::"decodes Ed25519 OKP public key"` AND `packages/vault/cbor/src/cose/cose-key.test.ts::"rejects unknown kty with UNEXPECTED_TYPE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: COSE ↔ JWK round-trip
|
||||
|
||||
The package SHALL export `coseKeyToJwk(key: CoseKey): JsonWebKey` and `jwkToCoseKey(jwk: JsonWebKey): CoseKey` round-trip helpers per RFC 9052 §7.1, RFC 8037 §2 (OKP), and RFC 7518 §6 (RSA / EC / symmetric). Round-trip SHALL be lossless for every supported key type — calling `jwkToCoseKey(coseKeyToJwk(key))` SHALL produce a CoseKey deeply equal to the input, and the reverse SHALL hold for any JWK constructible from a supported COSE key. RSA keys SHALL serialize `n` and `e` as base64url strings per RFC 7518; EC2/OKP coordinate buffers SHALL be base64url-encoded.
|
||||
|
||||
#### Scenario: ES256 EC2 round-trip
|
||||
|
||||
- **GIVEN** an RFC 9052 §7.1 sample ES256 EC2 key
|
||||
- **WHEN** `jwkToCoseKey(coseKeyToJwk(key))` is computed
|
||||
- **THEN** the result deeply equals the input key
|
||||
|
||||
#### Scenario: Ed25519 OKP round-trip
|
||||
|
||||
- **GIVEN** an RFC 8037 sample Ed25519 OKP key
|
||||
- **WHEN** the same round-trip is computed
|
||||
- **THEN** the result deeply equals the input
|
||||
|
||||
#### Scenario: RS256 RSA round-trip via base64url n/e
|
||||
|
||||
- **GIVEN** an RSA public key `{ kty: "RSA", n: <Uint8Array>, e: <Uint8Array> }`
|
||||
- **WHEN** `coseKeyToJwk(key)` is called
|
||||
- **THEN** the resulting JWK has `n` and `e` as base64url strings, and the round-trip back via `jwkToCoseKey` recovers the original buffers
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/cose/jwk.test.ts::"ES256 EC2 round-trip"` AND `packages/vault/cbor/src/cose/jwk.test.ts::"Ed25519 OKP round-trip"` AND `packages/vault/cbor/src/cose/jwk.test.ts::"RS256 RSA round-trip via base64url n/e"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: COSE alg label → genesis crypto-curve algorithm ID mapping
|
||||
|
||||
The package SHALL export `coseAlgToCryptoAlg(alg: number)` mapping every IANA-registered COSE algorithm label this package supports to the corresponding `@suaveplan/crypto-curve` algorithm ID per RFC 9053 §2: `-7` → ES256, `-8` → EdDSA (Ed25519), `-35` → ES384, `-36` → ES512, `-257` → RS256, `-258` → RS384, `-259` → RS512. Unknown labels SHALL throw `CborError(CBOR.UNEXPECTED_TYPE)`. The mapping SHALL be one-to-one and SHALL NOT silently substitute a "close enough" algorithm.
|
||||
|
||||
#### Scenario: ES256 maps to crypto-curve ES256 ID
|
||||
|
||||
- **WHEN** `coseAlgToCryptoAlg(-7)` is called
|
||||
- **THEN** the result is the `@suaveplan/crypto-curve` ES256 algorithm ID
|
||||
|
||||
#### Scenario: EdDSA maps to Ed25519
|
||||
|
||||
- **WHEN** `coseAlgToCryptoAlg(-8)` is called
|
||||
- **THEN** the result is the Ed25519 algorithm ID
|
||||
|
||||
#### Scenario: unknown label throws UNEXPECTED_TYPE
|
||||
|
||||
- **WHEN** `coseAlgToCryptoAlg(99999)` is called
|
||||
- **THEN** the call throws `CborError` with code `"CBOR.UNEXPECTED_TYPE"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/cose/alg-map.test.ts::"ES256 (-7) maps to crypto-curve ES256"` AND `packages/vault/cbor/src/cose/alg-map.test.ts::"EdDSA (-8) maps to Ed25519"` AND `packages/vault/cbor/src/cose/alg-map.test.ts::"unknown label throws UNEXPECTED_TYPE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Diagnostic notation emitter (RFC 8949 §8)
|
||||
|
||||
The package SHALL export `toDiagnostic(value): string` from the `./diagnostic` subpath that emits RFC 8949 §8 diagnostic notation for any decoded CBOR value. The emitter SHALL be debug-only — its sibling `.md` SHALL document that it is never to be invoked from encode/decode hot paths. Output SHALL match RFC 8949 §8 examples for the documented value forms (integers, floats with `_h_`, byte strings as `h''` hex, text strings as quoted, arrays / maps / tags with the documented separators).
|
||||
|
||||
#### Scenario: emits RFC 8949 §8 example for {1: 2, 3: 4}
|
||||
|
||||
- **WHEN** `toDiagnostic(new Map([[1, 2], [3, 4]]))` is called
|
||||
- **THEN** the result equals `"{1: 2, 3: 4}"`
|
||||
|
||||
#### Scenario: emits byte-string as h''
|
||||
|
||||
- **WHEN** `toDiagnostic(new Uint8Array([0x01, 0x02]))` is called
|
||||
- **THEN** the result equals `"h'0102'"`
|
||||
|
||||
#### Scenario: emits tagged value as N(...)
|
||||
|
||||
- **WHEN** `toDiagnostic(new Tagged(0, "2026-05-10T00:00:00Z"))` is called
|
||||
- **THEN** the result equals `"0(\"2026-05-10T00:00:00Z\")"`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/diagnostic/diagnostic.test.ts::"emits RFC §8 example map form"` AND `packages/vault/cbor/src/diagnostic/diagnostic.test.ts::"emits byte string as h''"` AND `packages/vault/cbor/src/diagnostic/diagnostic.test.ts::"emits tagged value as N(...)"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error registry — every code thrown by ≥1 production path
|
||||
|
||||
The package SHALL register exactly seven error codes via `defineCodes("CBOR", [...])`: `MALFORMED`, `UNEXPECTED_TYPE`, `UNKNOWN_TAG_STRICT`, `NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`, `NON_DETERMINISTIC`. Every code SHALL be thrown by at least one production code path inside `src/`; the registry SHALL NOT contain unused entries. The package SHALL export `CborError extends SuaveplanError<CborErrorCode>` where `CborErrorCode` is the literal-union of the seven codes.
|
||||
|
||||
#### Scenario: every code is reachable from src/
|
||||
|
||||
- **WHEN** the test scans `src/` for the throw site of each registered code
|
||||
- **THEN** every one of the seven codes is found in at least one production `.ts` file
|
||||
|
||||
#### Scenario: CborErrorCode literal union is the registry shape
|
||||
|
||||
- **WHEN** `expectTypeOf<CborErrorCode>().toEqualTypeOf<"CBOR.MALFORMED" | "CBOR.UNEXPECTED_TYPE" | "CBOR.UNKNOWN_TAG_STRICT" | "CBOR.NESTED_TOO_DEEP" | "CBOR.LENGTH_OVERFLOW" | "CBOR.INDEFINITE_NOT_ALLOWED" | "CBOR.NON_DETERMINISTIC">()` runs
|
||||
- **THEN** the type-level assertion holds
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/errors/error-codes.test.ts::"every registered code is thrown from production src"` AND `packages/vault/cbor/src/errors/error-codes.test.ts::"CborErrorCode literal union matches registry"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod boundary schemas at every public option type
|
||||
|
||||
The package SHALL declare Zod schemas (`EncoderOptionsSchema`, `DecoderOptionsSchema`, `TagHandlerSchema`, `CoseKeySchema` — discriminated by `kty` — and `JwkSchema`) and SHALL parse caller-supplied options at every public entry point (`encode`, `decode`, `decodeCoseKey`, `jwkToCoseKey`, `registry.register`). Schema failures SHALL surface as the genesis Zod-wrapped boundary error and SHALL NOT silently coerce malformed options into valid encodes / decodes.
|
||||
|
||||
#### Scenario: malformed EncoderOptions rejected at boundary
|
||||
|
||||
- **WHEN** `encode(0, { maxDepth: -1 } as never)` is called
|
||||
- **THEN** the call throws a Zod-validation error before any encoding takes place
|
||||
|
||||
#### Scenario: malformed DecoderOptions rejected at boundary
|
||||
|
||||
- **WHEN** `decode(bytes, { mode: "weird" as never })` is called
|
||||
- **THEN** the call throws a Zod-validation error
|
||||
|
||||
#### Scenario: CoseKeySchema validates discriminated union
|
||||
|
||||
- **WHEN** `CoseKeySchema.parse({ kty: "EC2", crv: "P-256", x: new Uint8Array(32), y: new Uint8Array(32) })` is called
|
||||
- **THEN** the result narrows to `Ec2Key`
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/schemas/schemas.test.ts::"EncoderOptionsSchema rejects negative maxDepth"` AND `packages/vault/cbor/src/schemas/schemas.test.ts::"DecoderOptionsSchema rejects unknown mode"` AND `packages/vault/cbor/src/schemas/schemas.test.ts::"CoseKeySchema discriminates by kty"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instrumentation
|
||||
|
||||
The package SHALL emit `cbor.encode_duration_ms` and `cbor.decode_duration_ms` histograms (units: milliseconds) wrapping every `encode` / `decode` call, a `cbor.tag_total{tag}` counter incremented per decoded tagged item (where `tag` is the numeric tag label as a string), and a `cbor.error_total{code}` counter incremented on every thrown `CborError` (where `code` is one of the seven registry codes). Instruments SHALL be sourced via `@opentelemetry/api` and SHALL be assertable under a real in-memory `MeterProvider`.
|
||||
|
||||
#### Scenario: encode emits histogram observation
|
||||
|
||||
- **GIVEN** a test in-memory `MeterProvider` registered globally
|
||||
- **WHEN** `encode(0)` is called
|
||||
- **THEN** the recorded histogram for `cbor.encode_duration_ms` has at least one observation
|
||||
|
||||
#### Scenario: decoded tag increments tag_total counter
|
||||
|
||||
- **GIVEN** a payload encoding a tag-32 URI value
|
||||
- **WHEN** `decode(bytes)` is called under the test meter
|
||||
- **THEN** `cbor.tag_total{tag="32"}` equals 1
|
||||
|
||||
#### Scenario: thrown CborError increments error_total counter
|
||||
|
||||
- **GIVEN** a malformed payload
|
||||
- **WHEN** `decode(bytes)` throws `CborError(CBOR.MALFORMED)`
|
||||
- **THEN** `cbor.error_total{code="CBOR.MALFORMED"}` equals 1
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/observability/metrics.test.ts::"encode records encode_duration_ms histogram"` AND `packages/vault/cbor/src/observability/metrics.test.ts::"decoded tag increments tag_total counter"` AND `packages/vault/cbor/src/observability/metrics.test.ts::"thrown CborError increments error_total counter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Property-based round-trip and deterministic-mode commutativity
|
||||
|
||||
The package SHALL ship fast-check property tests that prove encoder + decoder agree on the wire format under arbitrary structures: for any randomly-generated CBOR-representable value `v`, `decode(encode(v))` SHALL deeply equal `v`. The package SHALL also ship a property test proving deterministic-mode commutativity: for any logical map `m`, encoding `m` under `deterministic: true` SHALL produce byte-identical output regardless of the map's internal key ordering.
|
||||
|
||||
#### Scenario: round-trip property holds over arbitrary inputs
|
||||
|
||||
- **GIVEN** a fast-check arbitrary covering integers, floats, byte strings, text strings, arrays, maps, tagged items, and nested combinations
|
||||
- **WHEN** `decode(encode(v))` is computed for 1000 generated values
|
||||
- **THEN** every result deeply equals the input
|
||||
|
||||
#### Scenario: deterministic-mode commutativity holds over shuffled maps
|
||||
|
||||
- **GIVEN** a fast-check arbitrary that generates the same logical map with shuffled insertion order
|
||||
- **WHEN** `encode(map, { deterministic: true })` is computed for both shuffles across 1000 trials
|
||||
- **THEN** the two byte sequences are byte-identical for every trial
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/encoder/property.test.ts::"round-trip holds for arbitrary CBOR values"` AND `packages/vault/cbor/src/encoder/deterministic-property.test.ts::"deterministic encoding is commutative over shuffled map keys"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL declare subpath exports so consumers tree-shake to the minimum surface they need:
|
||||
|
||||
- `@suaveplan/cbor` — root barrel
|
||||
- `@suaveplan/cbor/encoder` — `Encoder`, `encode`, encoder types, half-float encode helper
|
||||
- `@suaveplan/cbor/decoder` — `Decoder`, `decode`, decoder types, well-formedness checker
|
||||
- `@suaveplan/cbor/streaming` — `PullDecoder`
|
||||
- `@suaveplan/cbor/sequences` — RFC 8742 `encodeSequence`, `decodeSequence`, `decodeSequenceStream`
|
||||
- `@suaveplan/cbor/tags` — `TagRegistry`, `Tagged`, built-in tag handlers
|
||||
- `@suaveplan/cbor/deterministic` — RFC 8949 §4.2 deterministic profile + dCBOR profile
|
||||
- `@suaveplan/cbor/cose` — `decodeCoseKey`, `coseKeyToJwk`, `jwkToCoseKey`, `coseAlgToCryptoAlg`, COSE types
|
||||
- `@suaveplan/cbor/diagnostic` — `toDiagnostic`
|
||||
- `@suaveplan/cbor/errors` — `CBOR_CODES`, `CborError`, `CborErrorCode`
|
||||
|
||||
#### Scenario: every subpath resolves
|
||||
|
||||
- **WHEN** every documented subpath is imported in a consumer test fixture
|
||||
- **THEN** every import resolves with no `Module not found` errors
|
||||
|
||||
**Verified by:** `packages/vault/cbor/src/index.test.ts::"every documented subpath resolves"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
226
openspec/archive/2026-05-10-enterprise-cbor/tasks.md
Normal file
226
openspec/archive/2026-05-10-enterprise-cbor/tasks.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Tasks — `@suaveplan/cbor` (RFC 8949 codec + RFC 9052 COSE bridge)
|
||||
|
||||
> Every checkbox below MUST reflect work actually done in this change. Do not retroactively tick boxes. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [ ] 1.1 Read `../suaveplan/enterprise/cbor.md` end-to-end
|
||||
- [ ] 1.2 Read RFC 8949 (CBOR), RFC 9052 (COSE), RFC 8742 (CBOR Sequences), draft-mcnally-deterministic-cbor (dCBOR) end-to-end
|
||||
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13, 14, 14a
|
||||
- [ ] 1.4 Confirm `proposal.md` covers every public-surface item, every subpath, and every dependency
|
||||
- [ ] 1.5 Confirm `specs/cbor/spec.md` carries one requirement per logical capability with ≥2 scenarios each
|
||||
- [ ] 1.6 Confirm every requirement has a `**Verified by:**` line naming concrete `it(...)` titles in concrete test file paths
|
||||
- [ ] 1.7 Source-mapping table cites every RFC section consumed
|
||||
|
||||
## 2. Package scaffold
|
||||
|
||||
- [ ] 2.1 Create `packages/vault/cbor/` with `src/{encoder,decoder,streaming,sequences,tags,cose,cwt,cddl,diagnostic,errors,observability,schemas,__benchmarks__}/`
|
||||
- [ ] 2.2 `package.json` — `@suaveplan/cbor@0.0.0`, `type: module`, `sideEffects: false`, deps include `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/crypto-curve`, `@suaveplan/crypto-cipher`, `@suaveplan/crypto-hash`, `@opentelemetry/api`, `tslib`, `zod` via catalog refs only (no bare versions)
|
||||
- [ ] 2.3 `package.json` `exports` map declares `.`, `./encoder`, `./decoder`, `./streaming`, `./sequences`, `./tags`, `./deterministic`, `./cose`, `./cwt`, `./cddl`, `./diagnostic`, `./errors` (12 subpaths) — each mapped to its built `dist/<feature>/<feature>.js` + `.d.ts` pair
|
||||
- [ ] 2.4 `tsconfig.lib.json` extends `@suaveplan/typescript-config/library.json`
|
||||
- [ ] 2.5 `vite.config.ts` with multi-entry build (one entry per subpath) + `vitest.config.ts` calling `createTestConfig()` from `@suaveplan/testing/config`
|
||||
- [ ] 2.6 `README.md` with package name, description, `bun add` install, quick-start example, Modules table linking every co-located `.md`
|
||||
|
||||
## 3. Implementation — encoder
|
||||
|
||||
- [ ] 3.1 `src/encoder/encoder.ts` — `Encoder` class + `encode(value, options?): Uint8Array` for major types 0–7 (RFC 8949 §3.1)
|
||||
- [ ] 3.2 Preferred-serialization shortest-form integer rule (RFC 8949 §4.2.1) — always-on
|
||||
- [ ] 3.3 `src/encoder/indefinite.ts` — opt-in indefinite-length arrays / maps / byte strings / text strings (RFC 8949 §3.2)
|
||||
- [ ] 3.4 `src/encoder/half-float.ts` — IEEE-754 binary16 encode (Major 7 / value 25); opt-in via `EncoderOptions.preferFloat16`
|
||||
- [ ] 3.5 `src/encoder/deterministic.ts` — RFC 8949 §4.2 deterministic profile (sorted map keys via lexicographic comparison over canonical encoded key bytes; shortest-form ints; no indefinite-length; no half-float)
|
||||
- [ ] 3.6 `src/encoder/dcbor.ts` — dCBOR profile under `./deterministic` subpath; reduction rules; rejects `undefined`
|
||||
- [ ] 3.7 `src/encoder/encoder.types.ts` — `EncoderOptions`, `IndefiniteLengthMarker`, related types
|
||||
|
||||
## 4. Implementation — decoder
|
||||
|
||||
- [ ] 4.1 `src/decoder/decoder.ts` — `Decoder` class + `decode(bytes, options?): unknown` for every major type
|
||||
- [ ] 4.2 `src/decoder/well-formed.ts` — `isWellFormed(bytes)` per RFC 8949 §5.1; trailing-byte rejection in strict mode
|
||||
- [ ] 4.3 `src/decoder/half-float.ts` — binary16 decode (always available)
|
||||
- [ ] 4.4 `src/decoder/decoder.types.ts` — `DecoderOptions`, `Tagged<T>`, `DecoderMode`
|
||||
- [ ] 4.5 Lax-mode opt-in (`mode: "lax"`) per RFC 8949 §5.2 basic validity only
|
||||
|
||||
## 5. Implementation — streaming pull-decoder
|
||||
|
||||
- [ ] 5.1 `src/streaming/pull-decoder.ts` — `PullDecoder` over `Uint8Array` and `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>` exposing `next(): Promise<IteratorResult<unknown>>`
|
||||
- [ ] 5.2 Bounded memory — heap delta under fixed cap regardless of stream size; verified by test
|
||||
- [ ] 5.3 `AbortSignal` honored — `next()` rejects with `CBOR.MALFORMED` when signal aborts mid-item
|
||||
|
||||
## 6. Implementation — sequences (RFC 8742)
|
||||
|
||||
- [ ] 6.1 `src/sequences/sequences.ts` — `encodeSequence(items, options?): Uint8Array`, `decodeSequence(bytes, options?): unknown[]`, `decodeSequenceStream(stream, options?): AsyncIterable<unknown>`
|
||||
- [ ] 6.2 Empty-sequence handling (returns empty `Uint8Array` / empty array)
|
||||
- [ ] 6.3 Strict-mode rejects partial trailing bytes; lax-mode tolerates and stops
|
||||
|
||||
## 7. Implementation — tag registry
|
||||
|
||||
- [ ] 7.1 `src/tags/registry.ts` — `TagRegistry` with `register`/`unregister`/`lookup`; double-register throws
|
||||
- [ ] 7.2 `src/tags/tagged.ts` — `Tagged<T>` value object
|
||||
- [ ] 7.3 Built-in tag handlers for 0 (date/time), 1 (epoch), 2 (positive bignum), 3 (negative bignum), 21 (base64url), 22 (base64), 23 (base16), 24 (encoded CBOR — re-enters decoder), 32 (URI), 35 (regex), 55799 (self-describe)
|
||||
- [ ] 7.4 Unknown tag in strict mode returns `Tagged{ tag, value }` rather than throwing (RFC 8949 §3.4 — unknown tags MUST NOT cause decode failure)
|
||||
|
||||
## 8. Implementation — COSE bridge (`./cose` subpath)
|
||||
|
||||
- [ ] 8.1 `src/cose/cose-key.ts` — `decodeCoseKey(bytes): CoseKey` returning `OkpKey | Ec2Key | RsaKey | SymmetricKey` per RFC 9052 §7
|
||||
- [ ] 8.2 `src/cose/jwk.ts` — `coseKeyToJwk(key): JsonWebKey` and `jwkToCoseKey(jwk): CoseKey` round-trip
|
||||
- [ ] 8.3 `src/cose/alg-map.ts` — `coseAlgToCryptoAlg(alg)` mapping IANA COSE alg labels to `@suaveplan/crypto-curve` algorithm IDs (-7 ES256, -8 EdDSA, -35 ES384, -36 ES512, -257 RS256, …)
|
||||
- [ ] 8.4 `src/cose/cose.types.ts` — `CoseKey` discriminated union by `kty`; `CoseAlg`, `CoseKty`, `CoseCurve`, `CoseKeyOp` literal unions
|
||||
- [ ] 8.5 `src/cose/headers.ts` — typed `CoseHeader` map + label registry (RFC 9052 §3.1: `alg` 1, `crit` 2, `content type` 3, `kid` 4, `IV` 5, `Partial IV` 6, `counter signature` 7); rejects malformed with `COSE_HEADER_INVALID`
|
||||
- [ ] 8.6 `src/cose/sig-structure.ts` — `buildSigStructure(context, protectedHeader, externalAad, payload)` per RFC 9052 §4.4 covering all five contexts (`Signature`, `Signature1`, `CounterSignature`, `MAC`, `MAC0`); deterministic-encoded
|
||||
- [ ] 8.7 `src/cose/sign1.ts` — `encodeCoseSign1` / `decodeCoseSign1` / `verifyCoseSign1` (RFC 9052 §4.2, CBOR tag 18); wires `@suaveplan/crypto-curve` ECDSA / EdDSA verifiers
|
||||
- [ ] 8.8 `src/cose/sign.ts` — `encodeCoseSign` / `decodeCoseSign` (RFC 9052 §4.1, CBOR tag 98); per-signer header carriage; `verifyCoseSign(msg, key)` returns matching signer index
|
||||
- [ ] 8.9 `src/cose/encrypt0.ts` — `encodeCoseEncrypt0` / `decodeCoseEncrypt0` / `decryptCoseEncrypt0` (RFC 9052 §5.2, CBOR tag 16); wires AES-GCM / ChaCha20-Poly1305 via `@suaveplan/crypto-cipher`
|
||||
- [ ] 8.10 `src/cose/encrypt.ts` — `encodeCoseEncrypt` / `decodeCoseEncrypt` (RFC 9052 §5.1, CBOR tag 96); per-recipient KEK lookup
|
||||
- [ ] 8.11 `src/cose/mac0.ts` — `encodeCoseMac0` / `decodeCoseMac0` / `verifyCoseMac0` (RFC 9052 §6.2, CBOR tag 17); wires HMAC-SHA-{256,384,512} via `@suaveplan/crypto-hash`
|
||||
- [ ] 8.12 `src/cose/mac.ts` — `encodeCoseMac` / `decodeCoseMac` (RFC 9052 §6.1, CBOR tag 97); multi-recipient MAC
|
||||
|
||||
## 8a. Implementation — CWT (`./cwt` subpath, RFC 8392)
|
||||
|
||||
- [ ] 8a.1 `src/cwt/claims.ts` — typed `CwtClaims` discriminator; IANA labels 1–7 (`iss` `sub` `aud` `exp` `nbf` `iat` `cti`) + `private[label]` slot
|
||||
- [ ] 8a.2 `src/cwt/encode.ts` — `encodeCwt(claims, options): Uint8Array` with deterministic profile; `Date` ↔ epoch via tag 1
|
||||
- [ ] 8a.3 `src/cwt/decode.ts` — `decodeCwt(bytes): CwtClaims`; rejects malformed with `UNEXPECTED_TYPE`
|
||||
- [ ] 8a.4 `src/cwt/verify.ts` — `verifyCwt(token, key, options): Promise<CwtVerifyResult>` orchestrating `verifyCoseSign1` / `verifyCoseMac0` plus time-window check via injectable `Clock`; throws `CWT_EXPIRED` / `CWT_NOT_YET_VALID` / `CWT_AUDIENCE_MISMATCH` / `CWT_ISSUER_MISMATCH` / `CWT_SIGNATURE_INVALID`
|
||||
- [ ] 8a.5 `src/cwt/cwt.types.ts` — `CwtClaims`, `CwtVerifyOptions`, `CwtVerifyResult`, `CwtPrivateClaim`
|
||||
|
||||
## 8b. Implementation — CDDL (`./cddl` subpath, RFC 8610)
|
||||
|
||||
- [ ] 8b.1 `src/cddl/lexer.ts` — RFC 8610 ABNF token stream (identifiers, literals, control operators, comments, ranges)
|
||||
- [ ] 8b.2 `src/cddl/parser.ts` — `parseCddl(source): CddlAst` covering every RFC 8610 §3 production; rejects malformed source with `CDDL_PARSE_FAILED` carrying `{ line, column }`
|
||||
- [ ] 8b.3 `src/cddl/ast.ts` — typed AST: `CddlAst`, `CddlRule`, `CddlType`, `CddlGroup`, `CddlOccurrence`, `CddlChoice`, `CddlControl`, `CddlGenericParam`
|
||||
- [ ] 8b.4 `src/cddl/built-ins.ts` — RFC 8610 §3.8 prelude (`uint`, `nint`, `int`, `bstr`, `tstr`, `bool`, `nil`, `null`, `undefined`, `tdate`, `time`, `number`, `text`, `bytes`, `float`, `float16`, `float32`, `float64`, `any`)
|
||||
- [ ] 8b.5 `src/cddl/control-ops.ts` — `.size`, `.bits`, `.regexp`, `.cbor`, `.within`, `.and`, comparison ops, `.default`
|
||||
- [ ] 8b.6 `src/cddl/validate.ts` — `validateCbor(value, schema, options): CddlValidationResult` walking decoded value against rule by name; emits `{ path, expected, got }` on mismatch; throws `CDDL_VALIDATION_FAILED` / `CDDL_RULE_NOT_FOUND` / `CDDL_UNDEFINED_REFERENCE`
|
||||
- [ ] 8b.7 `src/cddl/cddl.types.ts` — option / result types, error path discriminator
|
||||
|
||||
## 8c. Implementation — partial-item streaming (extends `./streaming`)
|
||||
|
||||
- [ ] 8c.1 `src/streaming/chunk-decoder.ts` — `ChunkDecoder` over `AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>` emitting `ChunkDecoderEvent` (`string-chunk` / `bytes-chunk` / `array-start` / `array-end` / `map-start` / `map-end` / `tag` / `value` / `break`)
|
||||
- [ ] 8c.2 Bounded memory regardless of item size — verified by 10 GiB indefinite-byte-string test asserting heap-delta cap
|
||||
- [ ] 8c.3 AbortSignal honored — emits final `STREAM_INTERRUPTED` error event mid-item
|
||||
- [ ] 8c.4 `src/streaming/chunk-decoder.types.ts` — `ChunkDecoderEvent` discriminated union; `ChunkDecoderOptions`
|
||||
|
||||
## 9. Implementation — diagnostic notation
|
||||
|
||||
- [ ] 9.1 `src/diagnostic/diagnostic.ts` — `toDiagnostic(value): string` per RFC 8949 §8; debug-only
|
||||
- [ ] 9.2 Sibling `.md` explicitly states "never invoke from hot paths"
|
||||
|
||||
## 10. Implementation — errors, schemas, observability
|
||||
|
||||
- [ ] 10.1 `src/errors/error-codes.ts` — `defineCodes("CBOR", [...])` registry: `MALFORMED`, `UNEXPECTED_TYPE`, `UNKNOWN_TAG_STRICT`, `NESTED_TOO_DEEP`, `LENGTH_OVERFLOW`, `INDEFINITE_NOT_ALLOWED`, `NON_DETERMINISTIC`, `STREAM_INTERRUPTED`, `COSE_SIG_INVALID`, `COSE_AEAD_FAILED`, `COSE_HEADER_INVALID`, `COSE_RECIPIENT_NOT_FOUND`, `CWT_EXPIRED`, `CWT_NOT_YET_VALID`, `CWT_AUDIENCE_MISMATCH`, `CWT_ISSUER_MISMATCH`, `CWT_SIGNATURE_INVALID`, `CDDL_PARSE_FAILED`, `CDDL_VALIDATION_FAILED`, `CDDL_RULE_NOT_FOUND`, `CDDL_UNDEFINED_REFERENCE`; `CborError extends SuaveplanError<CborErrorCode>`
|
||||
- [ ] 10.2 `src/observability/metrics.ts` — `cbor.encode_duration_ms` + `cbor.decode_duration_ms` histograms; `cbor.tag_total{tag}` + `cbor.error_total{code}` counters
|
||||
- [ ] 10.3 `src/schemas/schemas.ts` — Zod schemas for `EncoderOptions`, `DecoderOptions`, `TagHandler`, `CoseKey` discriminated union, `Jwk`
|
||||
- [ ] 10.4 Every code in the registry is thrown by ≥1 production code path; verified by `error-codes.test.ts`
|
||||
|
||||
## 11. Implementation — bounded recursion / item count
|
||||
|
||||
- [ ] 11.1 Encoder + decoder honor `maxDepth` (default 64); throw `CBOR.NESTED_TOO_DEEP` on overflow
|
||||
- [ ] 11.2 Decoder honors `maxItems` (default 1_000_000); throws `CBOR.LENGTH_OVERFLOW` when an indefinite-length container or a CBOR Sequence exceeds the cap
|
||||
- [ ] 11.3 Both caps configurable via `EncoderOptions` / `DecoderOptions`
|
||||
|
||||
## 12. Root barrel + subpath barrels
|
||||
|
||||
- [ ] 12.1 `src/index.ts` — explicit named re-exports (no `export *`); namespace re-exports allowed for sub-namespaces (e.g. `export * as cose from "./cose/index.js"` is permitted per CLAUDE.md §10)
|
||||
- [ ] 12.2 Every `<feature>/index.ts` uses explicit named re-exports per AGENTS.md §6a
|
||||
- [ ] 12.3 `package.json` `exports` map matches the 10 subpaths declared in §2.3
|
||||
|
||||
## 13. Tests (runner-neutral imports — `@suaveplan/testing/runner` only)
|
||||
|
||||
- [ ] 13.1 `src/encoder/encoder.test.ts` — major types 0–7; preferred-serialization shortest-form
|
||||
- [ ] 13.2 `src/encoder/encoder.spec-conformance.test.ts` — RFC 8949 Appendix A vectors round-trip
|
||||
- [ ] 13.3 `src/encoder/indefinite.test.ts` — opt-in emission; rejected under `deterministic: true`
|
||||
- [ ] 13.4 `src/encoder/half-float.test.ts` — binary16 encode for normal, subnormal, ±0, ±Inf, NaN, boundary values
|
||||
- [ ] 13.5 `src/encoder/deterministic.test.ts` — sorted keys; shortest-form; no indefinite; throws `NON_DETERMINISTIC` on forced indefinite
|
||||
- [ ] 13.6 `src/encoder/dcbor.test.ts` — dCBOR reduction rules; rejects `undefined`
|
||||
- [ ] 13.7 `src/encoder/property.test.ts` — fast-check round-trip on arbitrary structures
|
||||
- [ ] 13.8 `src/encoder/deterministic-property.test.ts` — fast-check byte-identical output across two runs on shuffled-key maps
|
||||
- [ ] 13.9 `src/decoder/decoder.test.ts` — every major type decode; `Tagged<T>` for unknown tags
|
||||
- [ ] 13.10 `src/decoder/decoder.spec-conformance.test.ts` — RFC 8949 §5.1 well-formedness rules; lax vs strict
|
||||
- [ ] 13.11 `src/decoder/well-formed.test.ts` — `isWellFormed` returns false for every §5.1 failure class
|
||||
- [ ] 13.12 `src/decoder/half-float.test.ts` — decode parity; subnormals; payload-preserving NaN
|
||||
- [ ] 13.13 `src/streaming/pull-decoder.test.ts` — one-item-per-`next()`; bounded memory under 1 GiB sequence; AbortSignal honored
|
||||
- [ ] 13.14 `src/sequences/sequences.test.ts` — RFC 8742 encode + decode; empty sequence; mixed types; partial-trailing-byte handling
|
||||
- [ ] 13.15 `src/tags/registry.test.ts` — register / lookup / unregister; double-register throws; unknown tag yields `Tagged`
|
||||
- [ ] 13.16 `src/tags/builtin.test.ts` — every built-in tag (0, 1, 2, 3, 21, 22, 23, 24, 32, 35, 55799) round-trips
|
||||
- [ ] 13.17 `src/cose/cose-key.test.ts` — RFC 9052 §7 EC2/OKP/RSA/symmetric sample keys decode to typed union
|
||||
- [ ] 13.18 `src/cose/jwk.test.ts` — ES256/Ed25519/RS256/symmetric round-trip
|
||||
- [ ] 13.19 `src/cose/alg-map.test.ts` — every supported alg label maps; unknown label throws `UNEXPECTED_TYPE`
|
||||
- [ ] 13.20 `src/diagnostic/diagnostic.test.ts` — RFC 8949 §8 sample strings produce byte-identical output
|
||||
- [ ] 13.21 `src/errors/error-codes.test.ts` — every registry code thrown by ≥1 production path; `expectTypeOf` pins the literal union
|
||||
- [ ] 13.22 `src/observability/metrics.test.ts` — histograms + counters recorded under in-memory `MeterProvider`
|
||||
- [ ] 13.23 `src/schemas/schemas.test.ts` — every Zod schema rejects malformed options
|
||||
- [ ] 13.24 `src/index.test.ts` — every documented subpath resolves
|
||||
- [ ] 13.25 `src/cose/sig-structure.test.ts` — RFC 9052 §4.4 vectors for all five contexts byte-identical
|
||||
- [ ] 13.26 `src/cose/sign1.test.ts` — RFC 9052 Appendix C.2 ES256 / EdDSA round-trip; tampered sig throws `COSE_SIG_INVALID`; tagged + tagless inputs
|
||||
- [ ] 13.27 `src/cose/sign.test.ts` — multi-signer encode + decode; verify-any returns matching index
|
||||
- [ ] 13.28 `src/cose/encrypt0.test.ts` — RFC 9052 Appendix C.3 AES-GCM-256 vector decrypts; tampered → `COSE_AEAD_FAILED`; ChaCha20-Poly1305 round-trip
|
||||
- [ ] 13.29 `src/cose/encrypt.test.ts` — multi-recipient round-trip; per-recipient KEK lookup
|
||||
- [ ] 13.30 `src/cose/mac0.test.ts` — HMAC-SHA-{256,384,512} round-trip; tampered MAC → `COSE_SIG_INVALID`
|
||||
- [ ] 13.31 `src/cose/mac.test.ts` — multi-recipient MAC round-trip
|
||||
- [ ] 13.32 `src/cose/headers.test.ts` — header label registry round-trip; malformed → `COSE_HEADER_INVALID`
|
||||
- [ ] 13.33 `src/cwt/claims.test.ts` — every IANA claim label round-trips; `Date` ↔ epoch tag 1
|
||||
- [ ] 13.34 `src/cwt/encode.test.ts` — deterministic claim-set encoding stable across runs
|
||||
- [ ] 13.35 `src/cwt/decode.test.ts` — RFC 8392 Appendix A vectors decode to typed claims
|
||||
- [ ] 13.36 `src/cwt/verify.test.ts` — `COSE_Sign1`-wrapped CWT verify; `CWT_EXPIRED` / `CWT_NOT_YET_VALID` / `CWT_AUDIENCE_MISMATCH` paths
|
||||
- [ ] 13.37 `src/cddl/lexer.test.ts` — token-stream coverage incl. comments, control ops, ranges
|
||||
- [ ] 13.38 `src/cddl/parser.test.ts` — every RFC 8610 §3 production; malformed source → `CDDL_PARSE_FAILED` with `{line,column}`
|
||||
- [ ] 13.39 `src/cddl/validate.test.ts` — RFC 8610 Appendix CWT + COSE_Sign schemas validate against real RFC vectors; mismatch surfaces `{path,expected,got}`
|
||||
- [ ] 13.40 `src/cddl/built-ins.test.ts` — every prelude type accepts conforming + rejects non-conforming
|
||||
- [ ] 13.41 `src/cddl/control-ops.test.ts` — every control operator tested both directions
|
||||
- [ ] 13.42 `src/streaming/chunk-decoder.test.ts` — event sequence for indefinite-length text-string in 3 chunks; bounded heap over 10 GiB synthetic byte-string; AbortSignal → `STREAM_INTERRUPTED`
|
||||
- [ ] 13.43 Every `it(...)` carries `{ timeout: 5000 }` (or `{ timeout: 30000 }` for the streaming + property tests)
|
||||
- [ ] 13.44 Runner-neutral imports — `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/vault/cbor/src/` returns zero output
|
||||
|
||||
## 14. Documentation
|
||||
|
||||
- [ ] 14.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference) — `encoder.md`, `indefinite.md`, `half-float.md`, `deterministic.md`, `dcbor.md`, `decoder.md`, `well-formed.md`, `pull-decoder.md`, `chunk-decoder.md`, `sequences.md`, `registry.md`, `tagged.md`, `builtin.md`, `cose-key.md`, `jwk.md`, `alg-map.md`, `headers.md`, `sig-structure.md`, `sign.md`, `sign1.md`, `encrypt.md`, `encrypt0.md`, `mac.md`, `mac0.md`, `claims.md` (cwt), `encode.md` (cwt), `decode.md` (cwt), `verify.md` (cwt), `lexer.md` (cddl), `parser.md` (cddl), `validate.md` (cddl), `built-ins.md` (cddl), `control-ops.md` (cddl), `diagnostic.md`, `error-codes.md`, `metrics.md`, `schemas.md`
|
||||
- [ ] 14.2 README.md links every co-located `.md` under `## Modules`
|
||||
- [ ] 14.3 Update `CLAUDE.md` package listing — add the `cbor` paragraph under the existing Vault grouping (after `vault-sync`)
|
||||
|
||||
## 15. Benchmarks
|
||||
|
||||
- [ ] 15.1 `src/__benchmarks__/encode.bench.ts` — encode 1 KiB COSE_Key, 100 KiB array of small items, 1 MiB nested map
|
||||
- [ ] 15.2 `src/__benchmarks__/decode.bench.ts` — decode the same payloads
|
||||
- [ ] 15.3 `src/__benchmarks__/cose-key.bench.ts` — `decodeCoseKey` on EC2/OKP/RSA samples
|
||||
- [ ] 15.4 README.md publishes baseline mitata numbers
|
||||
|
||||
## 16. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 16.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the monorepo
|
||||
- [ ] 16.2 `bunx turbo lint typecheck test --filter=@suaveplan/cbor` — green
|
||||
- [ ] 16.3 `bunx turbo test:coverage --filter=@suaveplan/cbor` — 100/100/100/100
|
||||
- [ ] 16.4 `bun scripts/openspec-conformance.ts @suaveplan/cbor` — exit 0, no `error` findings
|
||||
- [ ] 16.5 `bunx turbo build --filter=@suaveplan/cbor` — green
|
||||
|
||||
## 17. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [ ] 17.1 Generics threaded — `Tagged<T>`, `TagHandler<T>`, `PullDecoder` item type; `expectTypeOf` assertions on every literal-type claim (`CborErrorCode`, `CoseKey`, `CoseAlg`)
|
||||
- [ ] 17.2 Zod schemas wired at every declared boundary (`encode`, `decode`, `decodeCoseKey`, `jwkToCoseKey`, tag registration)
|
||||
- [ ] 17.3 Every error code in `error-codes.ts` thrown by ≥1 production path
|
||||
- [ ] 17.4 Strict-mode behaviors tested both on (`mode: "strict"`) and off (`mode: "lax"`)
|
||||
- [ ] 17.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
|
||||
- [ ] 17.6 Lifecycle dedup — N/A (codec is stateless beyond the optional `TagRegistry` instance; registry double-register throws and is tested)
|
||||
- [ ] 17.7 No retroactive tick — every box above reflects work done in this change
|
||||
|
||||
## 18. Banned-pattern + barrel-shape + runner-import scans
|
||||
|
||||
- [ ] 18.1 `grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" packages/vault/cbor/src/ --include="*.ts"` — zero output
|
||||
- [ ] 18.2 Barrel-shape scan per AGENTS.md §13 step 4a — zero output for any `<feature>/index.ts`
|
||||
- [ ] 18.3 Runner-import scan per AGENTS.md §13 step 4b — zero output
|
||||
- [ ] 18.4 Missing-`.md` scan — zero output
|
||||
- [ ] 18.5 Bare-version scan — zero output
|
||||
- [ ] 18.6 Subpath count matches feature module count (12 subpaths: `.`, `./encoder`, `./decoder`, `./streaming`, `./sequences`, `./tags`, `./deterministic`, `./cose`, `./cwt`, `./cddl`, `./diagnostic`, `./errors`)
|
||||
- [ ] 18.7 Security scan — no hardcoded secrets
|
||||
|
||||
## 19. Root-doc refresh
|
||||
|
||||
- [ ] 19.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md`
|
||||
- [ ] 19.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml`
|
||||
- [ ] 19.3 `STATUS.md` — move `cbor` row to `Completed`
|
||||
- [ ] 19.4 `SUMMARY.md` — add per-package paragraph
|
||||
- [ ] 19.5 `bunx markdownlint-cli2 --fix` then a second `bunx markdownlint-cli2` pass with exit 0
|
||||
- [ ] 19.6 `git status` shows only intentional changes
|
||||
|
||||
## 20. Archive
|
||||
|
||||
- [ ] 20.1 Fold spec delta into `openspec/specs/cbor/spec.md` (canonical, status ACTIVE)
|
||||
- [ ] 20.2 Move `openspec/changes/enterprise-cbor/` → `openspec/archive/<yyyy-mm-dd>-enterprise-cbor/`
|
||||
- [ ] 20.3 Confirm `MANIFEST.md` lists `@suaveplan/cbor` after refresh
|
||||
Reference in New Issue
Block a user