Files
suaveplan/openspec/archive/2026-05-10-enterprise-cbor/tasks.md
2026-05-27 23:29:53 +01:00

21 KiB
Raw Blame History

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